<html>
  <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <meta name="theme-color" content="#008000"/>
   <title>bow_ml</title>
   <link rel="manifest" href="bow_ml_manifest.json">
   <script>
    if('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/bow_ml_sw.js')
        .then(function() {
              console.log('Service Worker Registered');
        });
    }
   </script>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
  <h1>bow_ml</h1>
  <script src="three.js"></script>
  <script src="sprite3d.js"></script>
  <script>
(async () => {
    let container, scene, camera, session, controllerMeshes, bowMesh, stringMesh, pinchMeshes;
    let handTracker;

    /* const localTransformPositionVectors = {
      keyboard: new THREE.Vector3(0.015/2 * 3, 0, 0.015 * 4 * 3),
      hmd: new THREE.Vector3(0.015/2 * 3, 0, 0),
    };
    const localTransformRotationQuaterions = {
      keyboard: new THREE.Quaternion().setFromUnitVectors(
        new THREE.Vector3(-1, 0, 0),
        new THREE.Vector3(0, 0, -1)
      ).premultiply(
        new THREE.Quaternion().setFromAxisAngle(
          new THREE.Vector3(1, 0, 0),
          Math.PI / 4
        )
      ),
      hmd: new THREE.Quaternion().setFromAxisAngle(
        new THREE.Vector3(0, 0, 1),
        Math.PI / 4
      ).premultiply(new THREE.Quaternion().setFromUnitVectors(
        new THREE.Vector3(0, 0, -1),
        new THREE.Vector3(1, 0, 0)
      )),
    };
    const localStringPositionVectors = {
      keyboard: new THREE.Vector3(0, 0.015 * 12, 0.015 * 2),
      hmd: new THREE.Vector3(0, 0.015 * 2, 0),
    };
    const localStringRotationQuaterions = {
      keyboard: zeroQuaternion,
      hmd: new THREE.Quaternion().setFromUnitVectors(
        new THREE.Vector3(0, 0, 1),
        new THREE.Vector3(0, 1, 0)
      ),
    }; */
    const localTransformScaleVector = new THREE.Vector3(3, 3, 3);
    const localVector = new THREE.Vector3();
    const localVector2 = new THREE.Vector3();
    const localVector3 = new THREE.Vector3();
    const localVector4 = new THREE.Vector3();
    const localQuaternion = new THREE.Quaternion();
    const localMatrix = new THREE.Matrix4();
    // const localRay = new THREE.Ray();
    const localVectorFloat32Array = new Float32Array(3);
    const localVectorFloat32Array2 = new Float32Array(3);

    const zeroVector = new THREE.Vector3();
    const oneVector = new THREE.Vector3(1, 1, 1);
    const forwardVector = new THREE.Vector3(0, 0, -1);
    const upVector = new THREE.Vector3(0, 1, 0);
    const zeroQuaternion = new THREE.Quaternion();
    const forwardQuaternion = new THREE.Quaternion().setFromUnitVectors(upVector, forwardVector);

    const controllerGeometry = new THREE.CylinderBufferGeometry(0.001, 0.001, 1, 32, 1)
      .applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI / 2))
      .applyMatrix(new THREE.Matrix4().makeTranslation(0, 0, -0.5));
    const controllerMaterial = new THREE.MeshPhongMaterial({
      color: 0x4caf50,
    });
    const _makeControllerMesh = (x = 0, y = 0, z = 0, qx = 0, qy = 0, qz = 0, qw = 1) => {
      const mesh = new THREE.Mesh(controllerGeometry, controllerMaterial);
      mesh.position.set(x, y, z);
      mesh.quaternion.set(qx, qy, qz, qw);
      // mesh.matrix.compose(mesh.position, mesh.quaternion, mesh.scale);
      mesh.updateMatrix();
      mesh.updateMatrixWorld();
      mesh.matrixAutoUpdate = false;
      mesh.frustumCulled = false;
      return mesh;
    };
    const _getControllerIndex = inputSource => inputSource.handedness === 'left' ? 0 : 1;

    const _requestImage = url => new Promise((accept, reject) => {
      const img = new Image();

      img.onload = () => {
        accept(img);
      };
      img.onerror = err => {
        reject(img);
      };

      img.crossOrigin = 'Anonymous';
      img.src = url;
    });  const localLine = new THREE.Line3();

    const [
      bowImg,
      arrowImg,
    ] = await Promise.all([
      _requestImage('icons/Bow_Empty.png'),
      _requestImage('icons/Arrow.png'),
    ]);

    // const NPC_PLUGIN = 'npc';
    /* const OTHER_SIDES = {
      left: 'right',
      right: 'left',
    }; */
    const ARROW_SPEED = 0.05;
    const ARROW_GRAVITY = -10 / 1000 * 0.001;
    const ARROW_TERMINAL_VELOCITY = -10;
    const ARROW_TTL = 5 * 1000;
    const ARROW_LENGTH = 0.5;

    const pixelSize = 0.015;
    const bowHeight = 0.6;
    const arrowMatrix = (() => {
      const position = new THREE.Vector3(0, 0, -pixelSize*16);
      const rotation = new THREE.Quaternion().setFromAxisAngle(
        new THREE.Vector3(0, 0, 1),
        -Math.PI / 4
      ).premultiply(new THREE.Quaternion().setFromUnitVectors(
        new THREE.Vector3(0, 1, 0),
        new THREE.Vector3(0, 0, -1)
      )).premultiply(new THREE.Quaternion().setFromUnitVectors(
        new THREE.Vector3(1, 0, 0),
        new THREE.Vector3(0, 1, 0)
      ));
      const scale = new THREE.Vector3(1, 1, 1);
      return new THREE.Matrix4().compose(position, rotation, scale);
    })();
    const arrowTemplateMesh = sprite3d.makeSpriteMesh(arrowImg)
    arrowTemplateMesh.geometry.applyMatrix(arrowMatrix);
    const _makeArrowMesh = () => {
      const arrowMesh = arrowTemplateMesh.clone();
      arrowMesh.frustumCulled = false;
      arrowMesh.startTime = 0;
      arrowMesh.lastTime = 0;
      arrowMesh.velocity = new THREE.Vector3();
      arrowMesh.hitTesting = false;
      arrowMesh.updatePull = position => {
        if (position !== null) {
          arrowMesh.position.copy(position);
          arrowMesh.quaternion.setFromRotationMatrix(localMatrix.lookAt(
            position,
            localVector.copy(stringMesh.position)
              .add(
                localVector2.set(0, 0, -0.2)
                  .applyQuaternion(stringMesh.quaternion)
              ),
            localVector3.copy(upVector)
              .applyQuaternion(stringMesh.quaternion)
          ));
        } else {
          arrowMesh.position.copy(stringMesh.position);
          arrowMesh.quaternion.copy(stringMesh.quaternion)
            // .multiply(forwardQuaternion);
        }
        // arrowMesh.updateMatrixWorld();
      };

      return arrowMesh;
    };

    const stringGeometry = (() => {
      const geometry = new THREE.BoxBufferGeometry(pixelSize, bowHeight, pixelSize, 1, 2, 1)
        /* .applyMatrix(new THREE.Matrix4().makeRotationFromQuaternion(new THREE.Quaternion().setFromUnitVectors(
          new THREE.Vector3(0, 1, 0),
          new THREE.Vector3(0, 0, -1)
        )))
        .applyMatrix(new THREE.Matrix4().makeScale(3, 3 * 0.2, 3)); */
      const colors = new Float32Array(geometry.getAttribute('position').array.length);
      colors.fill(0x33/0xFF);
      geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
      return geometry;
    })();
    const stringGeometryPositions = stringGeometry.getAttribute('position').array;
    const numPositions = stringGeometryPositions.length / 3;
    const centerPositionIndexes = [];
    const centerPositions = [];
    for (let i = 0; i < numPositions; i++) {
      const baseIndex = i * 3;
      if (Math.abs(stringGeometryPositions[baseIndex + 1]) < 0.001) {
        centerPositionIndexes.push(baseIndex);
        centerPositions.push(
          stringGeometryPositions[baseIndex + 0],
          stringGeometryPositions[baseIndex + 1],
          stringGeometryPositions[baseIndex + 2]
        );
      }
    }
    if (centerPositionIndexes.length === 0 || centerPositions.length === 0) {
      throw new Error('failed to initializa string mesh');
    }
    /* const arrowGeometry = (() => {
      const {positions, normals, colors, dys} = arrowGeometrySpec;
      const geometry = new THREE.BufferGeometry();
      geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
      geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3));
      geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
      geometry.addAttribute('dy', new THREE.BufferAttribute(dys, 2));
      geometry.boundingSphere = new THREE.Sphere(
        new THREE.Vector3(),
        1
      );
      return geometry;
    })(); */
    const stringMaterial = new THREE.MeshBasicMaterial({
      vertexColors: THREE.VertexColors,
    });

    const _startNock = () => {
      if (!nockedArrowMesh) {
        nockedArrowMesh = _makeArrowMesh();
        scene.add(nockedArrowMesh);
      }
    };
    const _endNock = () => {
      if (nockedArrowMesh) {
        const arrow = nockedArrowMesh;
        arrows.push(arrow);

        const now = Date.now();
        arrow.startTime = now;
        arrow.lastTime = now;
        arrow.velocity.set(0, 0, -ARROW_SPEED * nockedArrowMesh.position.distanceTo(stringMesh.position))
          .applyQuaternion(arrow.quaternion);

        nockedArrowMesh = null;
      }
    };

    const _makeButtons = () => ({
      trigger: false,
      bumper: false,
    });
    const lastButtons = [
      _makeButtons(),
      _makeButtons(),
    ];
    const lastPincheds = [false, false];

    const grabbable = {
      isGrabbed() {
        return false;
      },
    };
    let grabbedIndex = -1;
    let pinchedIndex = -1;
    let pulling = false;
    let nockedArrowMesh = null;
    let drawnArrowMesh = null;
    const arrows = [];

    const _debounce = fn => {
      let running = false;
      let queued = false;

      const _go = () => {
        if (!running) {
          running = true;

          fn(() => {
            running = false;

            if (queued) {
              queued = false;

              _go();
            }
          });
        } else {
          queued = true;
        }
      };
      return _go;
    };

    async function init() {
      container = document.createElement('div');
      document.body.appendChild(container);

      scene = new THREE.Scene();
      scene.matrixAutoUpdate = false;
      // scene.background = new THREE.Color(0x3B3961);

      camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
      camera.position.set(0, 0, 1);
      // camera.lookAt(new THREE.Vector3());
      scene.add(camera);

      const ambientLight = new THREE.AmbientLight(0x808080);
      scene.add(ambientLight);

      const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
      directionalLight.position.set(1, 1, 1);
      scene.add(directionalLight);

      controllerMeshes = [
        _makeControllerMesh(-0.1),
        _makeControllerMesh(0.1),
      ];
      controllerMeshes.forEach(controllerMesh => {
        scene.add(controllerMesh);
      });

      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);

      // window.browser.magicleap.RequestDepthPopulation(true);
      // renderer.autoClear = false;

      container.appendChild(renderer.domElement);

      {
        bowMesh = sprite3d.makeSpriteMesh(bowImg);
        bowMesh.geometry
          .applyMatrix(new THREE.Matrix4().makeRotationFromQuaternion(
            new THREE.Quaternion().setFromAxisAngle(
              new THREE.Vector3(0, 0, 1),
              Math.PI / 4
            ).premultiply(new THREE.Quaternion().setFromUnitVectors(
              new THREE.Vector3(0, 0, -1),
              new THREE.Vector3(1, 0, 0)
            ))
          ))
          .applyMatrix(new THREE.Matrix4().makeTranslation(pixelSize, 0, 0));
        bowMesh.frustumCulled = false;
        scene.add(bowMesh);

        stringMesh = (() => {
          const geometry = stringGeometry;
          const material = stringMaterial;

          const stringMesh = new THREE.Mesh(geometry, material);
          stringMesh.frustumCulled = false;
          stringMesh.pullPosition = new THREE.Vector3();
          stringMesh.updatePull = position => {
            const pullPosition = position !== null ?
              localVector.copy(position)
                .applyMatrix4(localMatrix.getInverse(stringMesh.matrixWorld))
            :
              zeroVector;
            const positionAttribute = geometry.getAttribute('position');
            const positions = positionAttribute.array;
            for (let i = 0; i < centerPositionIndexes.length; i++) {
              const index = centerPositionIndexes[i];
              positions[index + 0] = centerPositions[i * 3 + 0] + pullPosition.x;
              positions[index + 1] = centerPositions[i * 3 + 1] + pullPosition.y;
              positions[index + 2] = centerPositions[i * 3 + 2] + pullPosition.z;
            }
            positionAttribute.needsUpdate = true;

            stringMesh.pullPosition.copy(pullPosition);
          };
          return stringMesh;
        })();
        scene.add(stringMesh);

        /* const _grab = e => {
          grabbable.setLocalTransform(localTransformPositionVectors[pose.getVrMode()], localTransformRotationQuaterions[pose.getVrMode()], localTransformScaleVector);
          stringMesh.visible = true;
        };
        grabbable.on('grab', _grab);
        const _release = e => {
          grabbable.setLocalTransform(zeroVector, zeroQuaternion, oneVector);
          stringMesh.visible = false;
        };
        grabbable.on('release', _release);

        const _gripdown = e => {
          const {side} = e;
          const otherSide = OTHER_SIDES[side];

          if (grabbable.isGrabbed() && grabbable.getGrabberSide() === otherSide) {
            const {gamepads} = pose.getStatus();
            const gamepad = gamepads[side];
            const {worldPosition: controllerPosition} = gamepad;
            stringMesh.getWorldPosition(localVector);

            if (controllerPosition.distanceTo(localVector) < 0.1) {
              pulling = true;
            } else {
              drawnArrowMesh = _makeArrowMesh();
              scene.add(drawnArrowMesh);

              // input.vibrate(side, 1, 20);
            }

            e.stopImmediatePropagation();
          }
        };
        input.on('gripdown', _gripdown, {
          priority: 1,
        });
        const _gripup = e => {
          const {side} = e;
          const otherSide = OTHER_SIDES[side];

          if (pulling && grabbable.getGrabberSide() === otherSide) {
            pulling = false;

            if (nockedArrowMesh) {
              const arrow = nockedArrowMesh;
              arrows.push(arrow);

              const now = Date.now();
              arrow.startTime = now;
              arrow.lastTime = now;
              arrow.velocity.set(0, 0, -ARROW_SPEED * nockedArrowMesh.distanceTo(stringMesh.position))
                .applyQuaternion(arrow.quaternion);

              nockedArrowMesh = null;
            }
          }
          if (drawnArrowMesh) {
            scene.remove(drawnArrowMesh);
            drawnArrowMesh = null;
          }
        };
        input.on('gripup', _gripup, {
          priority: 1,
        });
        const _triggerdown = e => {
          const {side} = e;

          if (pose.getVrMode() === 'keyboard' && !nockedArrowMesh) {
            nockedArrowMesh = _makeArrowMesh();
            scene.add(nockedArrowMesh);
          }
        };
        input.on('triggerdown', _triggerdown, {
          priority: 1,
        });
        const _triggerup = e => {
          const {side} = e;

          if (pose.getVrMode() === 'keyboard' && nockedArrowMesh) {
            const arrow = nockedArrowMesh;
            arrows.push(arrow);

            const now = Date.now();
            arrow.startTime = now;
            arrow.lastTime = now;
            arrow.velocity.set(0, 0, -ARROW_SPEED * 0.3)
              .applyQuaternion(arrow.quaternion);

            nockedArrowMesh = null;
          }
        };
        input.on('triggerup', _triggerup, {
          priority: 1,
        }); */

        const _makePinchMesh = () => {
          const geometry = new THREE.BoxBufferGeometry(0.01, 0.01, 0.01);
          const material = new THREE.MeshPhongMaterial({
            color: 0x2196f3,
          });
          const mesh = new THREE.Mesh(geometry, material);
          mesh.visible = false;
          mesh.frustumCulled = false;
          return mesh;
        };
        pinchMeshes = [
          _makePinchMesh(),
          _makePinchMesh(),
        ];
        pinchMeshes.forEach(pinchMesh => {
          scene.add(pinchMesh);
        });

        const _onHands = updates => {
          for (let i = 0; i < pinchMeshes.length; i++) {
            pinchMeshes[i].visible = false;
          }

          let pinchedSeen = false;
          for (let i = 0; i < updates.length; i++) {
            const update = updates[i];
            const {hand, pointer} = update;
            const handIndex = hand === 'left' ? 0 : 1;
            const lastPinched = lastPincheds[handIndex];
            const pinchMesh = pinchMeshes[handIndex];

            if (lastPinched) {
              if (pinchedIndex === -1) {
                localLine.set(
                  localVector
                    .copy(stringMesh.position)
                    .add(
                      localVector3
                        .set(0, bowHeight/2, 0)
                        .applyQuaternion(stringMesh.quaternion),
                    ),
                  localVector2
                    .copy(stringMesh.position)
                    .add(
                      localVector3
                        .set(0, -bowHeight/2, 0)
                        .applyQuaternion(stringMesh.quaternion)
                    )
                );
                const pinchPosition = localVector.fromArray(pointer.position);
                const closestStringPoint = localLine.closestPointToPoint(pinchPosition, true, localVector2);
                if (closestStringPoint.distanceTo(pinchPosition) < 0.05) {
                  pinchedIndex = handIndex;

                  _startNock();
                }
              }
              if (pinchedIndex === handIndex) {
                pinchedSeen = true;
              }
              
              pinchMesh.position.fromArray(pointer.position);
              pinchMesh.quaternion.fromArray(pointer.rotation);
              pinchMesh.visible = true;
            } else {
              if (pinchedIndex === handIndex) {
                pinchedIndex = -1;

                _endNock();
              }
            }
          }
          if (pinchedIndex !== -1 && !pinchedSeen) {
            pinchedIndex = -1;

            _endNock();
          }
        };
        const _onGesture = e => {
          const {hand, gesture, lastGesture, position, rotation} = e;
          // console.log('got gesture', {hand, gesture, lastGesture});

          const pinched = gesture === 'pinch';
          const handIndex = hand === 'left' ? 0 : 1;

          lastPincheds[handIndex] = pinched;
        };

        handTracker = window.browser.magicleap.RequestHandTracking();
        handTracker.onhands = _onHands;
        handTracker.ongesture = _onGesture;
      }

      renderer.setAnimationLoop(animate);
    }

    let lastPulling = false;
    function animate(time, frame) {
      if (renderer.vr.enabled) {
        const gamepads = navigator.getGamepads();
        const inputSources = session.getInputSources();

        const _updateControllerMeshes = () => {
          for (let i = 0; i < controllerMeshes.length; i++) {
            controllerMeshes[i].visible = false;
          }

          const inputSources = session.getInputSources();
          for (let i = 0; i < inputSources.length; i++) {
            const inputSource = inputSources[i];
            const pose = frame.getInputPose(inputSource);

            const controllerIndex = _getControllerIndex(inputSource);
            const controllerMesh = controllerMeshes[controllerIndex];
            controllerMesh.matrix.fromArray(pose.targetRay.transformMatrix);
            controllerMesh.matrix.decompose(controllerMesh.position, controllerMesh.quaternion, controllerMesh.scale);
            // controllerMesh.updateMatrixWorld(true);
            controllerMesh.visible = true;
          }
        };
        const _updateGamepads = () => {
          for (let i = 0; i < gamepads.length; i++) {
            const gamepad = gamepads[i];
            const bumper = gamepad.buttons[2].pressed;
            const lastBumper = lastButtons[i].bumper;

            if (bumper && !lastBumper) {
              const inputSource = inputSources[i];
              const pose = frame.getInputPose(inputSource);
              localMatrix
                .fromArray(pose.targetRay.transformMatrix)
                .decompose(localVector, localQuaternion, localVector2);
              if (localVector.distanceTo(bowMesh.position) < 0.3) {
                grabbedIndex = i;
              }
            } else if (lastBumper && !bumper) {
              if (grabbedIndex !== -1) {
                grabbedIndex = -1;
              }
            }

            lastButtons[i].bumper = bumper;
          }
        };
        const _updateGrab = () => {
          if (grabbedIndex !== -1) {
            const inputSource = inputSources[grabbedIndex];
            const pose = frame.getInputPose(inputSource);
            localMatrix
              .fromArray(pose.targetRay.transformMatrix)
              .decompose(bowMesh.position, bowMesh.quaternion, localVector2);
            stringMesh.position.copy(bowMesh.position);
            stringMesh.quaternion.copy(bowMesh.quaternion);
          }
        };
        const _updatePinch = () => {
          if (pinchedIndex !== -1) {
            const pinchMesh = pinchMeshes[pinchedIndex];
            stringMesh.updatePull(pinchMesh.position);
          } else {
            stringMesh.updatePull(null);
          }
        };
        const _updateNock = () => {
          if (nockedArrowMesh) {
            if (pinchedIndex !== -1) {
              const pinchMesh = pinchMeshes[pinchedIndex];
              nockedArrowMesh.updatePull(pinchMesh.position.clone());
            } else if (grabbedIndex !== -1) {
              const inputSource = inputSources[grabbedIndex];
              const pose = frame.getInputPose(inputSource);
              localMatrix
                .fromArray(pose.targetRay.transformMatrix)
                .decompose(localVector, localQuaternion, localVector2);
              localVector
                .add(
                  localVector2
                    .set(0, 0, 0.2)
                    .applyQuaternion(localQuaternion)
                );
              nockedArrowMesh.updatePull(localVector.clone());
            }
          }
        };
        /* const _updateStringPosition = () => {
          if (grabbable.isGrabbed()) {
            const side = grabbable.getGrabberSide();
            const gamepad = gamepads.find(gamepad => gamepad.hand === side);
            // const {worldPosition: controllerPosition, worldRotation: controllerRotation} = gamepad;
            stringMesh.position.copy(grabbable.position)
              .add(localVector.copy(localStringPositionVectors[pose.getVrMode()]).applyQuaternion(grabbable.rotation));
              // .add(localVector.set(0, 1, 0).applyQuaternion(grabbable.rotation));
            stringMesh.quaternion.copy(grabbable.rotation)
              .multiply(localStringRotationQuaterions[pose.getVrMode()]);
            stringMesh.updateMatrixWorld();
          }
        }; */
        /* const _updateDrawnArrow = () => {
          if (drawnArrowMesh) {
            const _updateTransform = () => {
              const side = grabbable.getGrabberSide();
              const otherSide = OTHER_SIDES[side];
              const gamepad = gamepads.find(gamepad => gamepad.hand === otherSide);
              const {worldPosition: controllerPosition, worldRotation: controllerRottion, worldScale: controllerScale} = gamepad;
              drawnArrowMesh.position.copy(controllerPosition);
              drawnArrowMesh.quaternion.copy(controllerRottion);
              drawnArrowMesh.scale.copy(controllerScale);
              drawnArrowMesh.updateMatrixWorld();
            };

            if (!nockedArrowMesh) {
              if (stringMesh.getWorldPosition(localVector).distanceTo(drawnArrowMesh.getWorldPosition(localVector2)) < 0.1) {
                nockedArrowMesh = drawnArrowMesh;
                drawnArrowMesh = null;
              } else {
                _updateTransform();
              }
            } else {
              _updateTransform();
            }
          }
        }; */
        /* const _updatePull = () => {
          const position = pulling ? gamepads[OTHER_SIDES[grabbable.getGrabberSide()]].worldPosition : null;

          if (pulling || lastPulling) {
            stringMesh.updatePull(position);
          }
          if (nockedArrowMesh) {
            nockedArrowMesh.updatePull(position);
          }

          lastPulling = pulling;
        }; */
        const _updateArrows = () => {
          if (arrows.length > 0) {
            const now = Date.now();

            const removedArrows = [];
            for (let i = 0; i < arrows.length; i++) {
              const arrow = arrows[i];
              const timeSinceStart = now - arrow.startTime;

              if (timeSinceStart < ARROW_TTL) {
                if (!arrow.velocity.equals(zeroVector)) {
                  /* const _hitNpc = () => {
                    const npcElement = elements.getEntitiesElement().querySelector(NPC_PLUGIN);
                    if (npcElement) {
                      localVector.copy(arrow.velocity).normalize();
                      localRay.set(arrow.position, arrow.velocity);
                      const hitNpc = npcElement.getHitNpc(localRay, ARROW_LENGTH);

                      if (hitNpc) {
                        hitNpc.attack();

                        removedArrows.push(arrow);

                        return true;
                      } else {
                        return false;
                      }
                    } else {
                      return false;
                    }
                  }; */
                  const _advanceArrow = () => {
                    const {velocity} = arrow;
                    const timeDiff = now - arrow.lastTime;
                    arrow.position.add(localVector.copy(velocity).multiplyScalar(timeDiff));
                    arrow.quaternion.setFromRotationMatrix(localMatrix.lookAt(
                      arrow.position,
                      localVector.copy(arrow.position)
                        .add(velocity),
                      upVector
                    ));
                    arrow.updateMatrixWorld();

                    velocity.y = Math.max(velocity.y + (ARROW_GRAVITY * timeDiff), ARROW_TERMINAL_VELOCITY);

                    arrow.lastTime = now;
                  };
                  const _checkCollision = () => {
                    if (!arrow.velocity.equals(zeroVector) && !arrow.hitTesting) {
                      const startPosition = arrow.position.clone();

                      session.requestHitTest(
                        arrow.position
                          .toArray(localVectorFloat32Array),
                        localVector.set(0, 0, -1)
                          .applyQuaternion(arrow.quaternion)
                          .toArray(localVectorFloat32Array2)
                      )
                        .then(result => {
                          if (result.length) {
                            const [{hitMatrix}] = result;
                            localMatrix
                              .fromArray(hitMatrix)
                              .decompose(localVector, localQuaternion, localVector2);

                            // console.log('check distance', localVector.distanceTo(startPosition), arrow.velocity.length()*50);
                            if (localVector.distanceTo(startPosition) < arrow.velocity.length()*50) {
                              console.log('got hit', localVector.toArray().join(','));
                              arrow.quaternion.setFromUnitVectors(
                                localVector2.set(0, 0, -1),
                                localVector3
                                  .copy(localVector)
                                  .sub(startPosition)
                                  .normalize()
                              );
                              arrow.position
                                .copy(localVector)
                                .add(
                                  localVector2.set(0, 0, 0.5)
                                    .applyQuaternion(arrow.quaternion)
                                );
                              arrow.updateMatrixWorld();

                              arrow.velocity.copy(zeroVector);
                            }
                          }

                          arrow.hitTesting = false;
                        });

                      arrow.hitTesting = true;
                    }
                  };

                  _advanceArrow();
                  _checkCollision();
                }
              } else {
                removedArrows.push(arrow);
              }
            }
            for (let i = 0; i < removedArrows.length; i++) {
              const arrow = removedArrows[i];
              scene.remove(arrow);
              arrows.splice(arrows.indexOf(arrow), 1);
            }
          }
        };

        _updateControllerMeshes();
        _updateGamepads();
        _updateGrab();
        _updatePinch();
        _updateNock();
        // _updateStringPosition();
        // _updateDrawnArrow();
        // _updatePull();
        _updateArrows();
      }

      renderer.render(scene, renderer.vr.enabled ? renderer.vr.getCamera(camera) : camera);
    }

    await init();

    console.log('request session');
    session = await navigator.xr.requestSession({
      exclusive: true,
    }).catch(err => Promise.resolve(null));

    if (session) {
      session.onselectstart = e => {
        if (grabbedIndex !== -1) {
          _startNock();
        }
      };
      session.onselectend = e => {
        _endNock();
      };

      // console.log('request first frame');
      session.requestAnimationFrame((timestamp, frame) => {
        renderer.vr.setSession(session, {
          frameOfReferenceType: 'stage',
        });

        const {views} = frame.getViewerPose();
        const viewport = session.baseLayer.getViewport(views[0]);
        const width = viewport.width;
        const height = viewport.height;

        renderer.setSize(width * 2, height);

        renderer.setAnimationLoop(null);

        renderer.vr.enabled = true;
        renderer.vr.setAnimationLoop(animate);

        console.log('running!');
      });
    } else {
      console.log('no xr devices');
    }

    // renderer.setAnimationLoop(animate);
})()
  .catch(err => {
    console.warn(err.stack);
  });
  </script>
  </body>
</html>
